﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Collections.ObjectModel;
using System.Reflection;

namespace Ganaye.EmbeddedDb
{
    public abstract class Table : IEnumerable, IDisposable, IHasEncoding
    {
        internal TableClusteredFile mFile;

        internal ReadOnlyCollection<TableColumn> mColumns;
        Dictionary<string, TableColumn> mColumnsByName;
        internal Type mProxyType;
        private static object[] NoObjects = new object[] { };
        internal char mDecimalSeparator = '.';
        protected string mTableName;
        //protected System.Reflection.ConstructorInfo mConstructor;
        protected NewRecordFunction mCreateRecord;



        public ReadOnlyCollection<TableColumn> Columns
        {
            get { return mColumns; }
        }

        protected internal delegate Record NewRecordFunction();

        public Table(string path)
        {
            System.IO.FileInfo fi = new System.IO.FileInfo(path);
            mTableName = System.IO.Path.GetFileNameWithoutExtension(fi.Name);
            mFile = new TableClusteredFile();

            mFile.Open(fi.FullName, new System.IO.FileStream( 
                fi.FullName, 
                System.IO.FileMode.OpenOrCreate, 
                System.IO.FileAccess.ReadWrite, 
                System.IO.FileShare.None, 
                4096, 
                true));
        }

        #region IEnumerable Members
        private bool mIsDisposed;

        IEnumerator IEnumerable.GetEnumerator()
        {
            return new TableEnumerator(this);
        }

        #endregion

        #region IDisposable Members

        public virtual void Dispose(bool isDisposing)
        {
            if (mIsDisposed) return;
            mIsDisposed = true;
            if (isDisposing)
            {
                mFile.Close();
                // managed
            }
            //unmanaged
        }

        void IDisposable.Dispose()
        {
            if (mIsDisposed) return;
            GC.SuppressFinalize(this);
            Dispose(true);
        }

        ~Table()
        {
            Dispose(false);
        }
        #endregion


        internal Record GetRecord(UInt32 recordNo)
        {
            var result = mFile.InternalGetRecord(recordNo, false);
            result.mTable = this;
            return result;
        }

        internal UInt32 RecordCount
        {
            get
            {
                return mFile.RecordCount;
            }
        }


        internal void OnFieldChanging<T>(Record record, object recordColumn, T originalValue, T newValue)
        {


        }

        internal void BuildColumnsFromDefinitions(TableDefinition tableDefinition)
        {
            // then we create columns
            var newColumns = new List<TableColumn>();
            var newColumnsByName = new Dictionary<string, TableColumn>(System.StringComparer.OrdinalIgnoreCase);
            int newHeaderSize = 32 + 32 * tableDefinition.Columns.Count + 1;
            int offset = 1;
            for (int i = 0; i < tableDefinition.Columns.Count; i++)
            {
                var cd = tableDefinition.Columns[i];
                TableColumn newColumn = TableColumn.GetNew(
                    this,
                    i,
                    offset,
                    cd
                    );

                offset += newColumn.Size;
                newColumns.Add(newColumn);
                newColumnsByName.Add(newColumn.Name, newColumn);
            }
            // then we should be creating a header
            mColumns = newColumns.AsReadOnly();
            mColumnsByName = newColumnsByName;
            int newRecordSize = offset;
            mFile.Initialize(newHeaderSize, newRecordSize);

            // we should also generate our proxyType here
            // and then call InternalSetProxyType
        }

        internal TableColumn GetColumnByName(string columnName, bool returnNull)
        {
            TableColumn result = null;
            if (mColumnsByName.TryGetValue(columnName, out result))
                return result;
            if (returnNull) return null;
            else throw new RuntimeException(string.Format("Unknown column {0}.", columnName));
        }

        internal TableColumn GetColumnByNumber(int columnIndex)
        {
            return mColumns[columnIndex];
        }

        internal void InternalSetProxyType(Type type)
        {
            mProxyType = type;
            BuildColumns();
        }

        protected virtual void BuildColumns()
        {
            BuildColumnsFromDefinitions(mFile.mTableDefinition);
            
            mFile.mNewRecordFunction = this.NewRecord;
            foreach (TableColumn col in mColumns)
            {
                col.mPropertyInfo = mProxyType.GetProperty(col.Name);
            }
            if (mFile.IsNew)
            {
                OnWriteHeader(true);
            }
        }

        #region IHasEncoding Members

        Encoding IHasEncoding.Encoding
        {
            get { return System.Text.Encoding.ASCII; }
        }

        char IHasEncoding.DecimalPoint
        {
            get { throw new NotImplementedException(); }
        }

        #endregion


        protected virtual void OnWriteHeader(bool rebuildHeader)
        {
            var tblDefs=mFile.mTableDefinition;
            if (rebuildHeader)
            {
                tblDefs.VerNumber = 0x83;
                UInt16 recordWidth = 1; // deleted flag is 1
                foreach (ColumnDefinition col in mFile.mTableDefinition.Columns)
                {
                    recordWidth += col.Size;
                }
                tblDefs.RecordWidth = recordWidth;
                tblDefs.HeaderWidth = (UInt16)(mFile.mTableDefinition.Columns.Count * 32 + 32 + 1);
            }
            tblDefs.NbRecords=mFile.RecordCount;
            Byte[] buff=SimpleSerializer.SerializeToBytes(this, mFile.mTableDefinition);
            mFile.WriteHeader(true, buff);
            if (rebuildHeader)
            {
                foreach (ColumnDefinition col in mFile.mTableDefinition.Columns)
                {
                    buff = SimpleSerializer.SerializeToBytes(this, col);
                    mFile.WriteHeader(false, buff);
                }
                mFile.WriteHeader(false,new byte[] {0x0d});
            }
        }

        internal Record NewRecord()
        {
            // we are meant to instantiate our proxy class here
            Record result = mCreateRecord();
            result.mTable = this;
            result.mRecordNo = mFile.GetNewRecordNo();
#if ASSERTS
            System.Diagnostics.Debug.Assert(result.mRecordNo != uint.MaxValue, "Invalid RecordNo");
#endif
            return (Record)result;
        }

        internal int StringCompare(string value1, string value2)
        {
            // this could change depending on table codepage/case sensitiveness
            return string.Compare(value1, value2, StringComparison.InvariantCultureIgnoreCase);
        }
    }

    public class Table<T> : Table, IEnumerable<T> where T : Record
    {
        static Table()
        {
        }

        public Table(string path)
            : base(path)
        {
            BuildColumns();
        }

        protected override void BuildColumns()
        {
            bool alreadyHasColumns = (mFile.mTableDefinition.Columns.Count > 0);

            foreach (System.Reflection.PropertyInfo pi in typeof(T).GetProperties(
                BindingFlags.Public
                | BindingFlags.Instance))
            {
                if (pi.CanRead && pi.CanWrite && pi.DeclaringType != typeof(Record))
                {
                    ColumnDefinition col = null;
                    bool found = false;
                    for (int i = 0; i < mFile.mTableDefinition.Columns.Count; i++)
                    {
                        col = mFile.mTableDefinition.Columns[i];
                        if (string.Equals(col.ColumnName, pi.Name, StringComparison.InvariantCultureIgnoreCase))
                        {
                            found = true;
                            if (col.FieldType == null)
                            {
                                col.FieldType = pi.PropertyType;
                            }
                            else if (col.FieldType != pi.PropertyType)
                            {

                                throw new RuntimeException(string.Format("Invalid field {0}.{1} ({2}) is declared in the table as {3}",
                                    pi.DeclaringType.Name,
                                    pi.Name,
                                    pi.PropertyType.Name,
                                    col.FieldType.Name));
                            }
                        }
                    }
                    if (!found)
                    {
                        if (alreadyHasColumns) throw new RuntimeException(
                            string.Format(
                            "The column {0} doesn't exist in the existing {1} file.",
                            pi.Name,
                            mTableName));

                                
                        ColumnType newType;
                        int newSize, newDecimals;
                        GetDefaultColumnType(pi.PropertyType, out newType, out newSize, out newDecimals);

                        col = new ColumnDefinition(pi.Name, newType);
                        col.Size = (byte)newSize;
                        col.Decimals = (byte)newDecimals;
                        mFile.mTableDefinition.Columns.Add(col);
                    }
                    col.mFieldType = pi.PropertyType;
                    col.BaseProperty = pi;
                    
                }
            }

            mProxyType = ProxyFactory.Instance.GenerateProxy(typeof(T),
                typeof(T).Name + "Proxy",
                mFile.mTableDefinition.Columns);
            var mi = mProxyType.GetMethod("CreateRecord", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
            mCreateRecord = (Table.NewRecordFunction)Delegate.CreateDelegate(typeof(NewRecordFunction), mi);
            base.BuildColumns();
        }
        #region IEnumerable<T> Members

        IEnumerator<T> IEnumerable<T>.GetEnumerator()
        {
            return new TableEnumerator<T>(this);
        }

        #endregion

        internal static void GetDefaultColumnType(Type fieldType, out ColumnType columnType, out int defaultWidth, out int defaultDecimals)
        {
            defaultDecimals = 0;
            if (fieldType == typeof(string))
            {
                columnType = ColumnType.DBASE3_CHARACTER;
                defaultWidth = 20;
            }
            else if (fieldType == typeof(int) || fieldType == typeof(int?))
            {
                columnType = ColumnType.DBASE3_NUMBER;
                defaultWidth = 12;
            }
            else if (fieldType == typeof(bool) || fieldType == typeof(bool?))
            {
                columnType = ColumnType.DBASE3_LOGICAL;
                defaultWidth = 1;
            }
            else if (fieldType == typeof(DateTime) || fieldType == typeof(DateTime?))
            {
                columnType = ColumnType.DBASE3_DATE;
                defaultWidth = 8;
            }
            else if (fieldType == typeof(Double) || fieldType == typeof(Double?))
            {
                columnType = ColumnType.DBASE3_NUMBER;
                defaultWidth = 12;
                defaultDecimals = 2;
            }
            else throw new RuntimeException(string.Format("Invalid fieldType {0}", fieldType.Name));
        }

        public new T NewRecord()
        {
            return (T)base.NewRecord();
        }

        //public new UInt32 RecordCount
        //{
        //    get { return mFile.RecordCount; }
        //}

        public void Close()
        {
            mFile.Close();
        }
    }
}
